package com.director.core;

import com.director.core.annotation.DirectParam;
import org.apache.commons.lang.StringUtils;

import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * @author Simone Ricciardi
 * @version 1.0, 10/23/2011
 */
@SuppressWarnings("unchecked")
public class DirectMethodParam {

   private String name;
   private int order;
   private Type type;
   private String defaultValue;
   private ParameterStrategy parameterStrategy = new OrderParameterStrategy();

   public DirectMethodParam(int order, Type type, DirectParam directParam) {
      this.order = order;
      this.type = type;
      if(directParam != null) {
         this.name = directParam.name();
         this.defaultValue = directParam.defaultValue();
         if(StringUtils.isNotBlank(this.name)) {
            this.parameterStrategy = new NamedParameterStrategy();
         }
      }
   }

   public Object parseFromTransactionData(DirectTransactionData data) {
      ParameterFactory parameterFactory = this.getParameterFactory();
      Class inputType = parameterFactory.getInputType();
      return parameterFactory.create(type, this.parameterStrategy.parse(data, inputType));
   }

   private ParameterFactory getParameterFactory() {

      DirectConfiguration cfg = DirectContext.get().getConfiguration();
      if(this.type instanceof ParameterizedType && Collection.class.isAssignableFrom((Class)((ParameterizedType)this.type).getRawType())) {
         Class collectionClass = (Class) ((ParameterizedType) type).getRawType();
         Class genericClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];
         ParameterFactory parameterFactory = cfg.getParameterFactory(genericClass);
         if(parameterFactory != null) {
            return new CollectionParameterFactory(collectionClass, genericClass, parameterFactory);
         }
      }

      if(this.type instanceof Class && ((Class) this.type).isArray()) {
         Class componentClass = ((Class) type).getComponentType();
         ParameterFactory parameterFactory = cfg.getParameterFactory(componentClass);
         if(parameterFactory != null) {
            return new ArrayParameterFactory(componentClass, parameterFactory);
         }
      }

      if(this.type instanceof Class && cfg.getParameterFactory(((Class) this.type))!=null) {
         return cfg.getParameterFactory(((Class) this.type));
      }

      return new ParameterFactory() {

         @Override
         public Class getInputType() {
            return (Class) ((type instanceof ParameterizedType) ? ((ParameterizedType) type).getRawType() : type);
         }

         @Override
         public Object create(Type type, Object input) {
            return input;
         }
      };
   }

   public Object getDefaultValue() {
      Class typeClass = (Class) this.type;
      return (typeClass.isPrimitive()) ? PrimitiveDefaults.getDefaultValue(typeClass) : null;
   }

   interface ParameterStrategy {
      Object parse(DirectTransactionData data, Class<Object> type);
   }

   class OrderParameterStrategy implements ParameterStrategy {

      @Override
      public Object parse(DirectTransactionData data, Class<Object> type) {
         return data.parseValue(order, type);
      }
   }

   class NamedParameterStrategy implements ParameterStrategy {

      @Override
      public Object parse(DirectTransactionData data, Class<Object> type) {
         Object value = data.parseValue(name, type);
         return value != null ? value : DirectContext.get().getConfiguration().getParser().parse(defaultValue, type);
      }
   }

   public static class PrimitiveDefaults {
      private static boolean DEFAULT_BOOLEAN;
      private static byte DEFAULT_BYTE;
      private static short DEFAULT_SHORT;
      private static int DEFAULT_INT;
      private static long DEFAULT_LONG;
      private static float DEFAULT_FLOAT;
      private static double DEFAULT_DOUBLE;

      public static Object getDefaultValue(Class clazz) {
         if(clazz.equals(boolean.class)) {
            return DEFAULT_BOOLEAN;
         } else if(clazz.equals(byte.class)) {
            return DEFAULT_BYTE;
         } else if(clazz.equals(short.class)) {
            return DEFAULT_SHORT;
         } else if(clazz.equals(int.class)) {
            return DEFAULT_INT;
         } else if(clazz.equals(long.class)) {
            return DEFAULT_LONG;
         } else if(clazz.equals(float.class)) {
            return DEFAULT_FLOAT;
         } else if(clazz.equals(double.class)) {
            return DEFAULT_DOUBLE;
         } else {
            throw new IllegalArgumentException("Class type " + clazz + " not supported");
         }
      }
   }

   static class CollectionParameterFactory implements ParameterFactory <Object[], Collection> {

      private Class<Collection> collectionClass;
      private Class genericClass;
      private ParameterFactory parameterFactory;

      CollectionParameterFactory(Class<Collection> collectionClass, Class genericClass, ParameterFactory parameterFactory) {
         this.collectionClass = collectionClass;
         this.genericClass = genericClass;
         this.parameterFactory = parameterFactory;
      }

      @Override
      public Class<Object[]> getInputType() {
         return (Class<Object[]>) Array.newInstance(this.parameterFactory.getInputType(), 0).getClass();
      }

      @Override
      public Collection create(Type type, Object[] inputCollection) {

         int size = inputCollection.length;
         Collection result = Set.class.isAssignableFrom(this.collectionClass) ? new HashSet(size) : new ArrayList(size);
         for(Object input : inputCollection) {
            result.add(parameterFactory.create(this.genericClass, input));
         }
         return result;
      }
   }

   static class ArrayParameterFactory implements ParameterFactory<Object[], Object[]> {

      private Class componentClass;
      private ParameterFactory parameterFactory;

      public ArrayParameterFactory(Class componentClass, ParameterFactory parameterFactory) {
         this.componentClass = componentClass;
         this.parameterFactory = parameterFactory;
      }

      @Override
      public Class<Object[]> getInputType() {
         return (Class<Object[]>) Array.newInstance(this.parameterFactory.getInputType(), 0).getClass();
      }

      @Override
      public Object[] create(Type type, Object[] inputCollection) {
         int idx = 0;
         Object[] result =  (Object[]) Array.newInstance(componentClass, inputCollection.length);
         for(Object input : inputCollection) {
            result[idx++] = this.parameterFactory.create(this.componentClass, input);
         }
         return result;
      }
   }
}
